﻿//  --------------------------------
//  Copyright (c) AW Systems, Inc. All rights reserved.
//  This source code is made available under the terms of the Microsoft Public License (Ms-PL)
//  http://www.codeplex.com/SLAM/license
//  ---------------------------------

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using AWS.SLAM.Configuration;
using AWS.SLAM.Data;
using AWS.SLAM.Events;
using AWS.SLAM.Logging;
using AWS.SLAM.SharePoint;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebControls;

namespace AWS.SLAM.Activation
{
    /// <summary>
    /// The SqlDataSynchronizeTypeProcessor consists of a method to be used by SLAM's IDataSynchronizer
    /// Synchronize method in which each type managed by SLAM is synchronized by using each type's
    /// IDataSynchronizerTypeProcessor.
    /// 
    /// SqlDataSynchronizeTypeProcessor is the default implementation of IDataSynchronizeTypeProcessor.
    /// Process expects to receive parameters specifying whether it should create database tables and whether
    /// it should populate those tables with data from SharePoint.
    /// </summary>
    public class SqlDataSynchronizeTypeProcessor : IDataSynchronizeTypeProcessor
    {
        #region Private State

        /// <summary>
        /// Internal repository/data access layer 
        /// </summary>
        private SqlDataRepository _database;

        /// <summary>
        /// SLAM Configuration
        /// </summary>
        private IConfigurationManager _configuration;

        /// <summary>
        /// SLAM Logger
        /// </summary>
        private ILogger _logger;

        #endregion

        #region Constructors

        /// <summary>
        /// Creates instance of SqlDataSynchronizeTypeProcessor for the given Configuration and Logger.
        /// </summary>
        /// <param name="configuration"></param>
        /// <param name="logger"></param>
        public SqlDataSynchronizeTypeProcessor(IConfigurationManager configuration, ILogger logger)
        {
            _configuration = configuration;
            string connectionString = "";
            if (_configuration.DataMapping is XmlSqlDataMapping)
            {
                connectionString = ((XmlSqlDataMapping)_configuration.DataMapping).GetConnectionString();
            }

            if (String.IsNullOrEmpty(connectionString))
            {
                connectionString = _configuration.GetSection<ConnectionStringSection>("ConnectionStrings").Default;
            }
            else
            {
                connectionString = _configuration.GetSection<ConnectionStringSection>("ConnectionStrings").GetConnectionString(connectionString);
            }

            _database = new SqlDataRepository(connectionString);
            _logger = logger;
        }

        /// <summary>
        /// Disposes of database connection on object destruction.
        /// </summary>
        ~SqlDataSynchronizeTypeProcessor()
        {
            _database.Dispose();
        }

        #endregion

        #region IDataSynchronizeTypeProcessor Members

        /// <summary>
        /// Performs the synchronization process for the passed in ConfiguredType.  Synchronizing a particular
        /// type consists of the following steps:
        /// 1. Create Item and Association Tables
        /// 2. Clear existing Tables
        /// 3. Populate Data - If Content Type, iterate through all Lists in all sites of the site collection
        /// and calls IItemUpdater.Update on each List Item in each List that contains the Content Type for this type.
        /// If it is a List, calls IItemUpdater.Update on each List Item.
        /// </summary>
        /// <param name="sharepointSiteLocation">Full URL location for the SharePoint Site to synchronize</param>
        /// <param name="parameters">A variable set of named parameters.  Expects at least a parameter called
        /// "ConfigType" containing the ConfiguredType to be processed, "CreateTable" indicating whether
        /// Process will creates the item and association tables for the given type, and "PopulateData"
        /// indicating whether data from SharePoint will be populated in the type's item and 
        /// association tables.</param>
        public void Process(string sharepointSiteLocation, IDictionary<string, object> parameters)
        {
            //parameters must contain a "ConfigType" to process
            if (parameters["ConfigType"] is ConfiguredType)
            {
                ConfiguredType inputType = (ConfiguredType)parameters["ConfigType"];
                _logger.LogMessage("Information", "Processing " + inputType.Name);

                SPSite site = new SPSite(sharepointSiteLocation);
                string siteCollection = site.ServerRelativeUrl.TrimStart('/');
                DataMapping currentDataMapping = _configuration.DataMapping;
                if (!String.IsNullOrEmpty(siteCollection) && _configuration.DataMappings.ContainsKey(siteCollection))
                {
                    currentDataMapping = _configuration.DataMappings[siteCollection];
                }

                //Recreate SqlDataRepository based on the Current DataMapping in case the connection string is not the default
                string connectionString = "";
                if (currentDataMapping is XmlSqlDataMapping)
                {
                    connectionString = ((XmlSqlDataMapping)currentDataMapping).GetConnectionString();
                }

                if (String.IsNullOrEmpty(connectionString))
                {
                    connectionString = _configuration.GetSection<ConnectionStringSection>("ConnectionStrings").Default;
                }
                else
                {
                    connectionString = _configuration.GetSection<ConnectionStringSection>("ConnectionStrings").GetConnectionString(connectionString);
                }

                _database = new SqlDataRepository(connectionString);

                //Get DataSchema from DataMapping
                string schemaName = ((XmlSqlDataMapping)currentDataMapping).GetDataSchema();

                _database.ResetVerifiedTableList();
                _database.EnsureDatabaseExists();

                if (!_database.VerifySchemaExists(schemaName))
                {
                    _database.CreateSchema(schemaName);
                }

                string itemTypeName = inputType.Name;

                //For SqlConfiguredTypes the type name to use for getting type
                //information from the configuration should be the Table name if present
                //otherwise it should be the regular type Name.
                if (inputType is SqlConfiguredType)
                {
                    SqlConfiguredType sqlType = (SqlConfiguredType)inputType;
                    if (!String.IsNullOrEmpty(sqlType.Table))
                    {
                        itemTypeName = sqlType.Table;
                    }
                }
                
                string tableName = GetTableName(itemTypeName);

                List<string> tablesToClearAfterSynch = new List<string>();

                //If the Item table doesn't exist for this type and the CreateTable parameter is true, create it.
                //Otherwise if it exists and data will be populated, clear it.
                if (!_database.VerifyTableExists(schemaName, tableName) && parameters.ContainsKey("CreateTable") && (bool)parameters["CreateTable"])
                {
                    _logger.LogMessage("Information", "Create table for " + tableName);
                    _database.CreateContentTable(schemaName, tableName, currentDataMapping.GetTypeFields(inputType.Id, inputType.Name) as IEnumerable<Field>);
                } 
                else if (parameters.ContainsKey("CreateTable") && (bool)parameters["CreateTable"] && parameters.ContainsKey("PopulateData") && (bool)parameters["PopulateData"])
                {
                    _logger.LogMessage("Information", "Prep table for synchronization: " + tableName);
                    _database.AddTableSynchronizationObjects(schemaName, tableName);
                    if (!tablesToClearAfterSynch.Contains(tableName))
                    {
                        tablesToClearAfterSynch.Add(tableName);
                    }
                    //_logger.LogMessage("Information", "Clear table for " + tableName);
                    //_database.ClearTable(_dataSchema, tableName);
                }

                _database.UpdateContentTable(schemaName, tableName, currentDataMapping.GetTypeFields(inputType.Id, inputType.Name) as IEnumerable<Field>);

                //Check each Association for the type being processed
                foreach (AssociationField association in currentDataMapping.GetTypeAssociations(inputType.Id, inputType.Name))
                {
                    if (!String.IsNullOrEmpty(association.AssociationName) && !String.IsNullOrEmpty(association.AssociatedTypeName))
                    {
                        //If this Association table doesn't exist and the CreateTable parameter is true, create it.  
                        //Otherwise if it exists and data will be populated, clear it.
                        if (!_database.VerifyTableExists(schemaName, GetTableName(association.AssociationName)) && parameters.ContainsKey("CreateTable") && (bool)parameters["CreateTable"])
                        {
                            _logger.LogMessage("Information", "Create table for " + association.AssociationName);
                            _database.CreateAssociationTable(schemaName, GetTableName(association.AssociationName), tableName, GetTableName(association.AssociatedTypeName));
                        }
                        else if (parameters.ContainsKey("PopulateData") && (bool)parameters["PopulateData"])
                        {
                            _logger.LogMessage("Information", "Prep association table for synchronization: " + association.AssociationName);
                            _database.AddAssociationTableSynchronizationObjects(schemaName, GetTableName(association.AssociationName), GetTableName(itemTypeName), GetTableName(association.AssociatedTypeName));
                            if (!tablesToClearAfterSynch.Contains(association.AssociationName))
                            {
                                tablesToClearAfterSynch.Add(association.AssociationName);
                            }
                            //_logger.LogMessage("Information", "Clear table for " + association.AssociationName);
                            //_database.ClearTable(_dataSchema, GetTableName(association.AssociationName));
                        }
                    }
                }

                bool clearItemsOnSynchFinish = true;

                try
                {
                    //Populate tables with data from SharePoint if the PopulateData parameter is true
                    if (parameters.ContainsKey("PopulateData") && (bool)parameters["PopulateData"])
                    {
                        _logger.LogMessage("Information", "Populating data for " + tableName);
                        using (SPSite outerSiteCollection = new SPSite(sharepointSiteLocation))
                        {
                            SPWeb currentWeb = null;

                            try
                            {
                                currentWeb = outerSiteCollection.OpenWeb();
                            }
                            catch
                            {
                                //failed
                            }

                            if (currentWeb != null)
                            {
                                SPSecurity.RunWithElevatedPrivileges(delegate()
                                {
                                    using (SPSite elevatedSite = new SPSite(currentWeb.Site.ID))
                                    {
                                        SPWeb elevatedWeb = null;
                                        try
                                        {
                                            //If Site is empty, open the Root Web
                                            if (String.IsNullOrEmpty(inputType.Site))
                                            {
                                                elevatedWeb = elevatedSite.RootWeb;
                                            }
                                            else
                                            {
                                                elevatedWeb = elevatedSite.AllWebs[inputType.Site];
                                            }
                                        }
                                        catch
                                        {
                                            //failed
                                        }

                                        if (elevatedWeb != null && elevatedWeb.Exists)
                                        {
                                            _logger.LogMessage("Information", "Populating data for " + tableName + " from site");

                                            if (!inputType.ContentType)
                                            {
                                                try
                                                {
                                                    SPList list = null;


                                                    //If ID is present, try to open the List by ID otherwise just
                                                    //let it fail...
                                                    if (!String.IsNullOrEmpty(inputType.Id))
                                                    {
                                                        list = elevatedWeb.Lists[new Guid(inputType.Id)];
                                                    }
                                                    else
                                                    {
                                                        list = elevatedWeb.Lists[inputType.Name];
                                                    }

                                                    if (list != null)
                                                    {
                                                        _logger.LogMessage("Information", "Updating data from List: " + list.RootFolder.ServerRelativeUrl + " for " + tableName);

                                                        //Call update for every item in the list
                                                        foreach (SPListItem listItem in list.Items)
                                                        {
                                                            SlamItemContext.ListItem = listItem;
                                                            SlamItemContext.ProcessedByContentType = false;
                                                            SlamItemContext.TypeName = inputType.Name;

                                                            //Get IItemUpdater for List type
                                                            IItemUpdater updater = ComponentService.GetInstance<IItemUpdater>(inputType.SiteCollection, inputType.Site, inputType.Id, itemTypeName);
                                                            updater.Update(listItem);
                                                        }


                                                        foreach (SPListItem listItem in list.Folders)
                                                        {
                                                            SlamItemContext.ListItem = listItem;
                                                            SlamItemContext.ProcessedByContentType = false;
                                                            SlamItemContext.TypeName = inputType.Name;

                                                            //Get IItemUpdater for List type
                                                            IItemUpdater updater = ComponentService.GetInstance<IItemUpdater>(inputType.SiteCollection, inputType.Site, inputType.Id, itemTypeName);
                                                            updater.Update(listItem);
                                                        }

                                                        SlamItemContext.ResetContext();
                                                    }
                                                }
                                                catch
                                                {
                                                    //Fails to here if list is not found...
                                                }

                                                //Check is the special AllSites option is set to true.  
                                                //If a type includes an "AllSites" attribute value that is set to
                                                //true event handlers are attached to ALL Lists with the type Name.
                                                //Therefore to process a type with AllSites specified as true
                                                //all of the other lists after the first above need processed.
                                                if (!String.IsNullOrEmpty(currentDataMapping.GetTypeAttribute(inputType.Id, inputType.Name, "AllSites")) && currentDataMapping.GetTypeAttribute(inputType.Id, inputType.Name, "AllSites").ToLower() == "true")
                                                {
                                                    try
                                                    {
                                                        for (int i = 0; i < elevatedSite.AllWebs.Count; i++)
                                                        {
                                                            SPWeb subWeb = elevatedSite.AllWebs[i];

                                                            SPList list = null;

                                                            //If ID is present, try to open the List by ID otherwise just
                                                            //let it fail...
                                                            if (!String.IsNullOrEmpty(inputType.Id))
                                                            {
                                                                list = subWeb.Lists[new Guid(inputType.Id)];
                                                            }
                                                            else
                                                            {
                                                                list = subWeb.Lists[inputType.Name];
                                                            }

                                                            if (list != null)
                                                            {
                                                                _logger.LogMessage("Information", "Updating data from List: " + list.RootFolder.ServerRelativeUrl + " for " + tableName);


                                                                //Call update for every item in the list
                                                                foreach (SPListItem listItem in list.Items)
                                                                {
                                                                    SlamItemContext.ListItem = listItem;
                                                                    SlamItemContext.ProcessedByContentType = false;
                                                                    SlamItemContext.TypeName = inputType.Name;
                                                                    //Get IItemUpdater for List type
                                                                    IItemUpdater updater = ComponentService.GetInstance<IItemUpdater>(inputType.SiteCollection, inputType.Site, inputType.Id, itemTypeName);
                                                                    updater.Update(listItem);
                                                                }

                                                                foreach (SPListItem listItem in list.Folders)
                                                                {
                                                                    SlamItemContext.ListItem = listItem;
                                                                    SlamItemContext.ProcessedByContentType = false;
                                                                    SlamItemContext.TypeName = inputType.Name;
                                                                    //Get IItemUpdater for List type
                                                                    IItemUpdater updater = ComponentService.GetInstance<IItemUpdater>(inputType.SiteCollection, inputType.Site, inputType.Id, itemTypeName);
                                                                    updater.Update(listItem);
                                                                }
                                                                SlamItemContext.ResetContext();
                                                            }

                                                            subWeb.Dispose();
                                                        }
                                                    }
                                                    catch (Exception exception)
                                                    {
                                                        _logger.LogMessage("Exception", exception.ToString());
                                                    }
                                                }
                                            }
                                            else //If type is a ContentType
                                            {

                                                SPContentType contentType = null;
                                                try
                                                {
                                                    contentType = elevatedWeb.ContentTypes[inputType.Name];
                                                }
                                                catch(Exception exception)
                                                {
                                                    _logger.LogMessage("Exception", exception.ToString());
                                                }

                                                if (contentType != null)
                                                {
                                                    try
                                                    {
                                                        //To process a Content Type, iterate through
                                                        //all sites in the site collection, find each
                                                        //List that contains this Content Type and call
                                                        //IItemUpdater.Update on every item that is of this
                                                        //Content Type.
                                                        for (int i = 0; i < elevatedSite.AllWebs.Count; i++)
                                                        {
                                                            try
                                                            {
                                                                SPWeb subWeb = elevatedSite.AllWebs[i];
                                                                foreach (SPList list in subWeb.Lists)
                                                                {
                                                                    if (list.ContentTypes.Contains(contentType))
                                                                    {
                                                                        _logger.LogMessage("Information", "Updating data of Content Type: " + contentType.Name + " from List: " + list.RootFolder.ServerRelativeUrl + " for " + tableName);
                                                                        
                                                                        foreach (SPListItem listItem in list.Items)
                                                                        {
                                                                            //Check if this SPListItem is of the Content Type being processed,
                                                                            //if it matches call Update.
                                                                            if (listItem.ContentType != null && listItem.ContentType.EqualsOrInherits(inputType.Name))
                                                                            {
                                                                                SlamItemContext.ListItem = listItem;
                                                                                SlamItemContext.ProcessedByContentType = true;
                                                                                SlamItemContext.TypeName = inputType.Name;

                                                                                //Get IItemUpdater for Content Type
                                                                                IItemUpdater updater = ComponentService.GetInstance<IItemUpdater>(elevatedSite.ServerRelativeUrl.TrimStart('/'), subWeb.Name, itemTypeName);
                                                                                updater.Update(listItem);
                                                                            }
                                                                        }
                                                                        SlamItemContext.ResetContext();

                                                                        //In case this Content Type applies to folders,
                                                                        //check the List's Folders as well.  These items would
                                                                        //not be processed above
                                                                        foreach (SPListItem listItem in list.Folders)
                                                                        {
                                                                            if (listItem.ContentType != null && listItem.ContentType.EqualsOrInherits(inputType.Name))
                                                                            {
                                                                                SlamItemContext.ListItem = listItem;
                                                                                SlamItemContext.ProcessedByContentType = true;
                                                                                SlamItemContext.TypeName = inputType.Name;

                                                                                //Get IItemUpdater for Content Type
                                                                                IItemUpdater updater = ComponentService.GetInstance<IItemUpdater>(elevatedSite.ServerRelativeUrl.TrimStart('/'), subWeb.Name, itemTypeName);
                                                                                updater.Update(listItem);
                                                                            }
                                                                        }
                                                                        SlamItemContext.ResetContext();
                                                                    }
                                                                }
                                                                subWeb.Dispose();
                                                            }
                                                            catch (Exception exception)
                                                            {
                                                                _logger.LogMessage("Exception", exception.ToString());
                                                            }
                                                        }
                                                    }
                                                    catch (Exception exception)
                                                    {
                                                        _logger.LogMessage("Exception", exception.ToString());
                                                    }
                                                }
                                            }
                                            elevatedWeb.Dispose();
                                        }
                                    }
                                });

                                currentWeb.Dispose();
                            }
                        }
                    }

                    site.Dispose();
                }
                catch (Exception exception)
                {
                    _logger.LogMessage("Exception", exception.ToString());
                    clearItemsOnSynchFinish = false;
                }
                finally
                {
                    if (tablesToClearAfterSynch.Contains(tableName))
                    {
                        int deleted = _database.DropTableSynchronizationObjects(schemaName, tableName, clearItemsOnSynchFinish);
                        _logger.LogMessage("Information", "Number of items removed from " + tableName + ": " + deleted.ToString());
                        tablesToClearAfterSynch.Remove(tableName);
                    }

                    foreach (AssociationField association in currentDataMapping.GetTypeAssociations(inputType.Id, inputType.Name))
                    {
                        if (tablesToClearAfterSynch.Contains(association.AssociationName))
                        {
                            int deleted = _database.DropTableSynchronizationObjects(schemaName, GetTableName(association.AssociationName), clearItemsOnSynchFinish);
                            _logger.LogMessage("Information", "Number of items removed from " + GetTableName(association.AssociationName) + ": " + deleted.ToString());
                            tablesToClearAfterSynch.Remove(association.AssociationName);
                        }
                    }
                }
            }
        }

        #endregion

        #region Private Members

        /// <summary>
        /// Helper method that strips the given type name of non-word characters and
        /// spaces to ensure a well-formed SQL table name.
        /// </summary>
        /// <param name="typeName"></param>
        /// <returns></returns>
        private string GetTableName(string typeName)
        {
            return Regex.Replace(typeName, "[\\W\\s]", "");
        }

        #endregion
    }
}

